package com.ruoyi.flow.flow.model;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.ruoyi.flow.activiti.domain.editor.AbstractModel;
import com.ruoyi.flow.activiti.domain.editor.AppDefinition;
import com.ruoyi.flow.activiti.domain.editor.model.Model;
import com.ruoyi.flow.activiti.model.editor.ModelKeyRepresentation;
import com.ruoyi.flow.activiti.model.editor.ModelRepresentation;
import com.ruoyi.flow.activiti.model.editor.decisiontable.DecisionTableDefinitionRepresentation;
import com.ruoyi.flow.activiti.domain.editor.model.ModelRepository;
import com.ruoyi.flow.activiti.security.SecurityUtils;
import com.ruoyi.flow.activiti.util.XmlUtil;
import com.ruoyi.flow.core.exception.BadRequestException;
import com.ruoyi.flow.core.exception.InternalServerErrorException;
import com.ruoyi.flow.flow.FlowConstants;
import com.ruoyi.flow.activiti.service.api.ModelService;
import com.google.common.collect.Lists;
import org.activiti.bpmn.BpmnAutoLayout;
import org.activiti.bpmn.converter.BpmnXMLConverter;
import org.activiti.bpmn.model.*;
import org.activiti.bpmn.model.Process;
import org.activiti.dmn.model.DmnDefinition;
import org.activiti.dmn.xml.converter.DmnXMLConverter;
import org.activiti.editor.dmn.converter.DmnJsonConverter;
import org.activiti.editor.language.json.converter.BpmnJsonConverter;
import org.activiti.editor.language.json.converter.util.CollectionUtils;
import org.activiti.engine.RepositoryService;
import org.activiti.engine.RuntimeService;
import org.activiti.engine.TaskService;
import org.activiti.engine.identity.User;
import org.activiti.engine.repository.Deployment;
import org.activiti.engine.repository.DeploymentBuilder;
import org.activiti.engine.repository.ProcessDefinition;
import org.activiti.engine.runtime.ProcessInstance;
import org.activiti.engine.task.Task;
import org.activiti.form.model.FormDefinition;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.transaction.Transactional;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamReader;
import java.io.ByteArrayInputStream;
import java.io.InputStreamReader;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @Author: wtian
 * @Description:
 * @Date: Created in 10:21 2018/8/13
 * @Version: 1.0
 */
@Service
@PropertySource(value = {"classpath:workflow.properties"})
public class FlowModelService {
    private static final Logger LOGGER = LoggerFactory.getLogger(FlowModelService.class);
    @Value("${activiti.bpmn.dir}")
    private String bpmnDir;

    @Autowired
    private RepositoryService repositoryService;
    @Autowired
    private ModelRepository modelRepository;
    @Autowired
    private RuntimeService runtimeService;
    @Autowired
    private TaskService taskService;
    @Autowired
    protected ModelService modelService;
    @Autowired
    protected ObjectMapper objectMapper;

    protected DmnJsonConverter dmnJsonConverter = new DmnJsonConverter();
    protected DmnXMLConverter dmnXMLConverter = new DmnXMLConverter();
    protected BpmnXMLConverter bpmnXmlConverter = new BpmnXMLConverter();
    protected BpmnJsonConverter bpmnJsonConverter = new BpmnJsonConverter();

    public String getBpmnDir() {
        return bpmnDir;
    }

    /**
     * 获取模型列表
     *
     * @param deployed 是否部署。0：全部；1：未部署；2：已部署
     * @return
     */
    public List<com.ruoyi.flow.flow.model.ModelBean> list(int deployed) {
        List<Model> models = modelRepository.findModelsByModelType(FlowConstants.ModelType.MODEL.getModelType());
        List<com.ruoyi.flow.flow.model.ModelBean> modelBeans = Lists.newArrayList();
        ModelBean modelBean = null;
        for (Model model : models) {
            modelBean = new ModelBean(model);
            List<Deployment> relation = repositoryService.createDeploymentQuery().deploymentName(model.getName()).list();
            if (deployed == 1) {//只获取未部署
                if (relation != null && !relation.isEmpty()) {
                    continue;
                }
            } else if (deployed == 2) {//只获取已部署
                if (relation == null || relation.isEmpty()) {
                    continue;
                }
            }
            if (relation != null && !relation.isEmpty()) {
                modelBean.setDeployId(relation.get(0).getId());
                //针对已部署的，获取最新的definitionId
                ProcessDefinition definition = repositoryService.createProcessDefinitionQuery().deploymentId(modelBean.getDeployId()).latestVersion().singleResult();
                if (definition != null) {
                    modelBean.setDefinitionId(definition.getId());
                    modelBean.setDefinitionKey(definition.getKey());
                }
            }
            modelBeans.add(modelBean);
        }
        return modelBeans;
    }

    @Transactional
    public String deploy(String modelId, User user) {
        Model model = modelRepository.findById(modelId).get();
        DeploymentBuilder deploymentBuilder = repositoryService.createDeployment().name(model.getName()).key(model.getKey());
        List<Model> referencedModels = modelRepository.findModelsByParentModelId(model.getId());
        Map<String, Model> formMap = new HashMap<String, Model>();
        Map<String, Model> decisionTableMap = new HashMap<String, Model>();
        for (Model childModel : referencedModels) {
            if (Model.MODEL_TYPE_FORM == childModel.getModelType()) {
                formMap.put(childModel.getId(), childModel);

            } else if (Model.MODEL_TYPE_DECISION_TABLE == childModel.getModelType()) {
                decisionTableMap.put(childModel.getId(), childModel);
            }
        }
        BpmnModel bpmnModel = modelService.getBpmnModel(model, formMap, decisionTableMap);
        Map<String, StartEvent> startEventMap = processNoneStartEvents(bpmnModel);

        for (Process process : bpmnModel.getProcesses()) {
            processUserTasks(process.getFlowElements(), process, startEventMap);
        }

        byte[] modelXML = modelService.getBpmnXML(bpmnModel);
        deploymentBuilder.addInputStream(model.getKey().replaceAll(" ", "") + ".bpmn", new ByteArrayInputStream(modelXML));
        if (formMap.size() > 0) {
            for (String formId : formMap.keySet()) {
                Model formInfo = formMap.get(formId);
                deploymentBuilder.addString("form-" + formInfo.getKey() + ".form", formInfo.getModelEditorJson());
            }
        }

        if (decisionTableMap.size() > 0) {
            for (String decisionTableId : decisionTableMap.keySet()) {
                Model decisionTableInfo = decisionTableMap.get(decisionTableId);
                try {
                    JsonNode decisionTableNode = objectMapper.readTree(decisionTableInfo.getModelEditorJson());
                    DmnDefinition dmnDefinition = dmnJsonConverter.convertToDmn(decisionTableNode, decisionTableInfo.getId(),
                            decisionTableInfo.getVersion(), decisionTableInfo.getLastUpdated());
                    byte[] dmnXMLBytes = dmnXMLConverter.convertToXML(dmnDefinition);
                    deploymentBuilder.addBytes("dmn-" + decisionTableInfo.getKey() + ".dmn", dmnXMLBytes);

                } catch (Exception e) {
                    LOGGER.error("Error converting decision table to XML " + decisionTableInfo.getName(), e);
                    throw new InternalServerErrorException("Error converting decision table to XML " + decisionTableInfo.getName());
                }
            }
        }

        Deployment deployment = deploymentBuilder.deploy();
        return deployment.getId();
    }

    private void processUserTasks(Collection<FlowElement> flowElements, Process process, Map<String, StartEvent> startEventMap) {

        for (FlowElement flowElement : flowElements) {
            if (flowElement instanceof UserTask) {
                UserTask userTask = (UserTask) flowElement;
                if ("$INITIATOR".equals(userTask.getAssignee())) {
                    if (startEventMap.get(process.getId()) != null) {
                        userTask.setAssignee("${" + startEventMap.get(process.getId()).getInitiator() + "}");
                    }
                }

            } else if (flowElement instanceof SubProcess) {
                processUserTasks(((SubProcess) flowElement).getFlowElements(), process, startEventMap);
            }
        }
    }

    private Map<String, StartEvent> processNoneStartEvents(BpmnModel bpmnModel) {
        Map<String, StartEvent> startEventMap = new HashMap<String, StartEvent>();
        for (Process process : bpmnModel.getProcesses()) {
            for (FlowElement flowElement : process.getFlowElements()) {
                if (flowElement instanceof StartEvent) {
                    StartEvent startEvent = (StartEvent) flowElement;
                    if (CollectionUtils.isEmpty(startEvent.getEventDefinitions())) {
                        if (StringUtils.isEmpty(startEvent.getInitiator())) {
                            startEvent.setInitiator("initiator");
                        }
                        startEventMap.put(process.getId(), startEvent);
                        break;
                    }
                }
            }
        }
        return startEventMap;
    }


    /**
     * 获取流程模型
     *
     * @param modelKey 模型标识
     * @return
     */
    public Model getModel(String modelKey) {
        return modelService.getModelByKey(modelKey);
    }

    /**
     * 删除模型
     *
     * @param definitionId
     * @return
     */
    public boolean deleteModel(String definitionId) {
        List<ProcessDefinition> definitions = repositoryService.createProcessDefinitionQuery().processDefinitionId(definitionId).list();
        for (ProcessDefinition definition : definitions) {
            String deployId = definition.getDeploymentId();
            org.activiti.engine.repository.Model model = repositoryService.createModelQuery().deploymentId(deployId).singleResult();
            BpmnModel bpmnModel = repositoryService.getBpmnModel(definitionId);
            if (model != null) {
                String model_Id = model.getId();
                // 删除模型
                repositoryService.deleteModel(model_Id);
            }
            List<ProcessInstance> instances = runtimeService.createProcessInstanceQuery().processDefinitionId(definitionId).list();
            List<Task> tasks = taskService.createTaskQuery().deploymentId(deployId).list();
            // 删除实例
            for (ProcessInstance instance : instances) {
                runtimeService.deleteProcessInstance(instance.getProcessInstanceId(), "test");
            }

            // 删除Task
            for (Task task : tasks) {
                taskService.deleteTask(task.getId());
            }
            // 删除部署信息和流程定义信息
            repositoryService.deleteDeployment(deployId);
        }
        return true;
    }

    /**
     * 新建模型
     *
     * @param modelRepresentation
     * @return
     */
    public Model createModel(ModelRepresentation modelRepresentation) {
        modelRepresentation.setKey(modelRepresentation.getKey().replaceAll(" ", ""));

        ModelKeyRepresentation modelKeyInfo = modelService.validateModelKey(null, modelRepresentation.getModelType(), modelRepresentation.getKey());
        if (modelKeyInfo.isKeyAlreadyExists()) {
            throw new BadRequestException("Provided model key already exists: " + modelRepresentation.getKey());
        }

        String json = null;
        if (modelRepresentation.getModelType() != null && modelRepresentation.getModelType().equals(AbstractModel.MODEL_TYPE_FORM)) {
            try {
                json = objectMapper.writeValueAsString(new FormDefinition());
            } catch (Exception e) {
                LOGGER.error("Error creating form model", e);
                throw new InternalServerErrorException("Error creating form");
            }

        } else if (modelRepresentation.getModelType() != null && modelRepresentation.getModelType().equals(AbstractModel.MODEL_TYPE_DECISION_TABLE)) {
            try {
                DecisionTableDefinitionRepresentation decisionTableDefinition = new DecisionTableDefinitionRepresentation();

                String decisionTableDefinitionKey = modelRepresentation.getName().replaceAll(" ", "");
                decisionTableDefinition.setKey(decisionTableDefinitionKey);

                json = objectMapper.writeValueAsString(decisionTableDefinition);
            } catch (Exception e) {
                LOGGER.error("Error creating decision table model", e);
                throw new InternalServerErrorException("Error creating decision table");
            }

        } else if (modelRepresentation.getModelType() != null && modelRepresentation.getModelType().equals(AbstractModel.MODEL_TYPE_APP)) {
            try {
                json = objectMapper.writeValueAsString(new AppDefinition());
            } catch (Exception e) {
                LOGGER.error("Error creating app definition", e);
                throw new InternalServerErrorException("Error creating app definition");
            }

        } else {
            ObjectNode editorNode = objectMapper.createObjectNode();
            editorNode.put("id", "canvas");
            editorNode.put("resourceId", "canvas");
            ObjectNode stencilSetNode = objectMapper.createObjectNode();
            stencilSetNode.put("namespace", "http://b3mn.org/stencilset/bpmn2.0#");
            editorNode.put("stencilset", stencilSetNode);
            ObjectNode propertiesNode = objectMapper.createObjectNode();
            propertiesNode.put("process_id", modelRepresentation.getKey());
            propertiesNode.put("name", modelRepresentation.getName());
            if (StringUtils.isNotEmpty(modelRepresentation.getDescription())) {
                propertiesNode.put("documentation", modelRepresentation.getDescription());
            }
            editorNode.put("properties", propertiesNode);

            ArrayNode childShapeArray = objectMapper.createArrayNode();
            editorNode.put("childShapes", childShapeArray);
            ObjectNode childNode = objectMapper.createObjectNode();
            childShapeArray.add(childNode);
            ObjectNode boundsNode = objectMapper.createObjectNode();
            childNode.put("bounds", boundsNode);
            ObjectNode lowerRightNode = objectMapper.createObjectNode();
            boundsNode.put("lowerRight", lowerRightNode);
            lowerRightNode.put("x", 130);
            lowerRightNode.put("y", 193);
            ObjectNode upperLeftNode = objectMapper.createObjectNode();
            boundsNode.put("upperLeft", upperLeftNode);
            upperLeftNode.put("x", 100);
            upperLeftNode.put("y", 163);
            childNode.put("childShapes", objectMapper.createArrayNode());
            childNode.put("dockers", objectMapper.createArrayNode());
            childNode.put("outgoing", objectMapper.createArrayNode());
            childNode.put("resourceId", "startEvent1");
            ObjectNode stencilNode = objectMapper.createObjectNode();
            childNode.put("stencil", stencilNode);
            stencilNode.put("id", "StartNoneEvent");
            json = editorNode.toString();
        }

        Model newModel = modelService.createModel(modelRepresentation, json, SecurityUtils.getCurrentUserObject());
        return newModel;
    }

    /**
     * 导入模型
     *
     * @param request
     * @param file
     * @return
     */
    public ModelRepresentation upload(HttpServletRequest request, MultipartFile file) {
        String fileName = file.getOriginalFilename();
        if (fileName != null && (fileName.endsWith(".bpmn") || fileName.endsWith(".bpmn20.xml"))) {
            try {
                XMLInputFactory xif = XmlUtil.createSafeXmlInputFactory();
                InputStreamReader xmlIn = new InputStreamReader(file.getInputStream(), "UTF-8");
                XMLStreamReader xtr = xif.createXMLStreamReader(xmlIn);
                BpmnModel bpmnModel = bpmnXmlConverter.convertToBpmnModel(xtr);
                if (CollectionUtils.isEmpty(bpmnModel.getProcesses())) {
                    throw new BadRequestException("No process found in definition " + fileName);
                }

                if (bpmnModel.getLocationMap().size() == 0) {
                    BpmnAutoLayout bpmnLayout = new BpmnAutoLayout(bpmnModel);
                    bpmnLayout.execute();
                }

                ObjectNode modelNode = bpmnJsonConverter.convertToJson(bpmnModel);

                Process process = bpmnModel.getMainProcess();
                String name = process.getId();
                if (StringUtils.isNotEmpty(process.getName())) {
                    name = process.getName();
                }
                String description = process.getDocumentation();

                ModelRepresentation model = new ModelRepresentation();
                model.setKey(process.getId());
                model.setName(name);
                model.setDescription(description);
                model.setModelType(AbstractModel.MODEL_TYPE_BPMN);
                Model newModel = modelService.createModel(model, modelNode.toString(), SecurityUtils.getCurrentUserObject());
                return new ModelRepresentation(newModel);

            } catch (BadRequestException e) {
                throw e;

            } catch (Exception e) {
                LOGGER.error("Import failed for " + fileName, e);
                throw new BadRequestException("Import failed for " + fileName + ", error message " + e.getMessage());
            }
        } else {
            throw new BadRequestException("Invalid file name, only .bpmn and .bpmn20.xml files are supported not " + fileName);
        }
    }

}
